/*
 * TimingsLog.java    ver   29/nov/2009
 *
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.commons.timer;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.mentalsmash.documentation.Concurrency;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.internal.Maps;

/**
 * Basic implementation of {@link TimingsLog}
 * <p>
 * See {@link TimingsLog} for more details.
 * </p>
 * 
 * @version 0.0.1 29/nov/2009
 * @author Andrea Reale <andrea.reale@gmail.com>
 * @see TimingsLog
 */
public class TimingsLogImpl implements TimingsLog {

    private final int historySize;
    private final Map<String, TimingEntry> timeMeasures;

    @Inject
    TimingsLogImpl(final int historySize) {

	timeMeasures = Maps.newHashMap();
	this.historySize = historySize;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.aop.TimingsLog#addTimingMeasure(java
     * .lang.String, long)
     */
    public void addTimingMeasure(final String key, final long measure) {
	Preconditions.checkNotNull(key);
	Preconditions.checkArgument(measure > 0);
	final TimingEntry entry;

	synchronized (timeMeasures) {
	    if (timeMeasures.get(key) == null) {
		timeMeasures.put(key, new TimingEntry());
	    }

	    entry = timeMeasures.get(key);
	    entry.addMeasure(measure);
	}

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.aop.TimingsLog#getLastTimeMeasures
     * (java.lang.String)
     */
    public List<Long> getTimeMeasures(final String key) {

	synchronized (timeMeasures) {
	    final TimingEntry entry = timeMeasures.get(key);
	    if (entry == null) {
		return null;
	    }

	    return ImmutableList.copyOf(entry.getRecentHistory());
	}

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.mentalsmash.tazio.transactions.aop.TimingsLog#getLastTimeMeasure(
     * java.lang.String)
     */
    public long getLastTimeMeasure(final String key) {
	synchronized (timeMeasures) {
	    final TimingEntry entry = timeMeasures.get(key);
	    if (entry == null) {
		return -1;
	    } else {
		return entry.getRecentHistory().getLast();
	    }
	}
    }

    @Override
    public long getMaxMeasure(final String key) {
	synchronized (timeMeasures) {
	    final TimingEntry entry = timeMeasures.get(key);
	    if (entry == null) {
		return -1;
	    } else {
		return entry.getMaxMeasure();
	    }
	}
    }

    @Override
    public long getMinMeasure(final String key) {
	synchronized (timeMeasures) {
	    final TimingEntry entry = timeMeasures.get(key);
	    if (entry == null) {
		return -1;
	    } else {
		return entry.getMinMeasure();
	    }
	}
    }
    
    @Override
    public double getAvgMeasure(final String key) {
	synchronized (timeMeasures) {
	    final TimingEntry entry = timeMeasures.get(key);
	    if (entry == null) {
		return -1;
	    } else {
		return entry.getAvgMeasure();
	    }
	}
    }
    
    @Override
    public double getStdDeviation(final String key) {
	synchronized (timeMeasures) {
	    final TimingEntry entry = timeMeasures.get(key);
	    if (entry == null) {
		return -1;
	    } else {
		return entry.getStdDeviation();
	    }
	}
    }

    @Concurrency(concurrentAccess = false, specification = "all accesses are synchronized by the enclosing class")
    private class TimingEntry {

	private final LinkedList<Long> recentHistory;
	private long maxMeasure = -1;
	private long minMeasure = -1;
	
	private long numOfObservations = 0;
	private double mean = 0;
	private double m2 = 0;

	TimingEntry() {
	    recentHistory = Lists.newLinkedList();
	}

	public void addMeasure(final long measure) {
	    if (this.minMeasure == -1 || measure < this.minMeasure) {
		this.minMeasure = measure;
	    }

	    if (this.maxMeasure == -1 || measure > this.maxMeasure) {
		this.maxMeasure = measure;
	    }

	    while (recentHistory.size() >= historySize) {
		recentHistory.removeFirst();
	    }
	    recentHistory.addLast(measure);
	    numOfObservations++;
	    double delta = measure - this.mean;
	    this.mean += (delta/numOfObservations);
	    this.m2 += delta*(measure - this.mean);
	    	   
	}

	/**
	 * @return the recentHistory
	 */
	public LinkedList<Long> getRecentHistory() {
	    return recentHistory;
	}

	/**
	 * @return the maxMeasure
	 */
	public long getMaxMeasure() {
	    return maxMeasure;
	}

	/**
	 * @return the minMeasure
	 */
	public long getMinMeasure() {
	    return minMeasure;
	}

	public double getAvgMeasure() {
	    return this.mean;
	}
	
	public double getStdDeviation() {
	    double variance = this.m2 / (numOfObservations-1);
	    return Math.sqrt(variance);
	}

    }

    @Override
    public Iterable<String> getCurrentKeys() {
	return this.timeMeasures.keySet();
    }

}
